#!/bin/sh
# tlp-func-rf-sw - Radio Switch Functions
#
# Copyright (c) 2024 Thomas Koch <linrunner at gmx.net> and others.
# SPDX-License-Identifier: GPL-2.0-or-later

# Needs: tlp-func-base, tlp-func-rf

# shellcheck disable=SC2034

# ----------------------------------------------------------------------------
# Constants

readonly NMCLI=nmcli
readonly RFKILL="rfkill"
readonly RFKD="/dev/rfkill"

readonly ALLDEV="bluetooth nfc wifi wwan"

readonly RDW_NM_LOCK="rdw_nm"
readonly RDW_DOCK_LOCK="rdw_dock"
readonly RDW_NM_LOCKTIME=2
readonly RDW_KILL="rdw_kill"

readonly RFSTATEFILE=$VARDIR/rfkill_saved

# ----------------------------------------------------------------------------
# Functions

get_devc () { # get control device for radio type
              # $1: rftype bluetooth/nfc/wifi/wwan
              # retval $_devc: sysdev, $_devs: device state
              #     $_rfkdev: 1/0=is/is not an rfkill device,
              #     $_devon, $_devoff: value to write directly to the sysdev
              #     to achieve the desired switch state

    local i

    # preset retvals
    _devc=""
    _devs=254
    _rfkdev="1"
    _devon="1"
    _devoff="0"

    case "$1" in
        wwan|bluetooth|nfc)
            for i in /sys/class/rfkill/rfkill* ; do
                if [ "$(read_sysf "$i/type")" = "$1" ]; then
                    _devc="$i/state"
                    echo_debug "rf" "get_devc($1) = $_devc"
                    return 0
                fi
            done
            ;;

        wifi)
            for i in /sys/bus/pci/drivers/ipw2?00/*/rf_kill; do
                if [ -f "$i" ]; then
                    _devc="$i"
                    _rfkdev="0"
                    _devon="0"
                    _devoff="1"
                    echo_debug "rf" "get_devc($1) = $_devc"
                    return 0
                fi
            done

            for i in /sys/class/rfkill/rfkill* ; do
                if [ "$(read_sysf "$i/type")" = "wlan" ]; then
                    _devc="$i/state"
                    echo_debug "rf" "get_devc($1) = $_devc"
                    return 0
                fi
            done
            ;;

        *)
            cecho "Error: unknown device type $1" 1>&2
            echo_debug "rf" "get_devc($1).unknown_type"
            return 0
            ;;
    esac

    echo_debug "rf" "get_devc($1).not_present"

    return 0
}

get_devs () { # get radio device state -- $1: rftype; retval $_devs: 0=off/1=on
    if [ -n "$_devc" ]; then
        _devs="$(read_sysf "$_devc")"
        case "$_devs" in
            0|1) # invert state when not a rfkill device
                [ "$_rfkdev" = "0" ] && _devs=$((_devs ^ _devoff))
                ;;
            2) ;; # hard blocked device
            *) _devs=3 # invalid state
        esac
    fi

    echo_debug "rf" "get_devs($1) = $_devs"

    return 0
}

err_no_root_priv () { # check root privilege
    cecho "Error: missing root privilege." 1>&2
    echo_debug "rf" "$1.missing_root_privilege"

    return 0
}

test_rfkill_perms () { # test if either root priv or rfkill device writable
    test_root || [ -w $RFKD ]
}

check_nm () { # test if NetworkManager is installed
    [ "$X_USE_NMCLI" != "0" ] && cmd_exists $NMCLI
}

invoke_nmcli () { # call nmcli with radio option according to the program version
                  # $1: rftype, $2: on/off, $3: caller; rc: last nmcli rc
    local rc

    check_nm || return 0 # return if NetworkManager not running

    $NMCLI radio "$1" "$2" > /dev/null 2>&1; rc=$?
    echo_debug "rf" "invoke_nmcli($1, $2).radio: rc=$rc"

    return $rc
}

device_state () { # get radio type state -- $1: rftype; retval $_devc, $_devs: 0=off/1=on
    echo_debug "rf" "device_state($1)"

    get_devc "$1"
    get_devs "$1"
}

device_switch () { # switch radio type state
                   # $1: rftype, $2: 1/on/0/off/toggle
                   # $3: lock id, $4: lock duration
                   # rc: 0=switched/1=invalid device or operation/
                   #     2=hard blocked/3=invalid state/4=no change
                   # retval $_devc, $_devs: 0=off/1=on

    local curst newst devn

    echo_debug "rf" "device_switch($1, $2, $3, $4)"

    get_devc "$1"

    # quit if no device
    if [ -z "$_devc" ]; then
        echo_debug "rf" "device_switch($1, $2).no_device: rc=1"
        return 1
    fi

    # quit if invalid operation
    if ! wordinlist "$2" "on 1 off 0 toggle"; then
        echo_debug "rf" "device_switch($1, $2).invalid_op: rc=1"
        return 1
    fi

    # get current device state
    get_devs "$1"
    curst="$_devs"

    # quit if device state is hard blocked or invalid
    if [ "$_devs" -ge 2 ]; then
        case "$_devs" in
            2) echo_debug "rf" "device_switch($1, $2).hard_blocked: rc=$_devs" ;;
            *) echo_debug "rf" "device_switch($1, $2).invalid_state: rc=$_devs" ;;
        esac
        return "$_devs"
    fi

    # determine desired device state
    case "$2" in
        1|on)   newst=1 ;;
        0|off)  newst=0 ;;
        toggle) newst=$((curst ^ 1)) ;;
    esac

    # compare current and desired device state
    if [ "$curst" = "$newst" ]; then
        # desired matches current state --> do nothing
        echo_debug "rf" "device_switch($1, $2).desired_state"

    else
        # desired does not match current state --> do switch

        # set timed lock if required
        [ -n "$3" ] && [ -n "$4" ] && [ "$1" != "bluetooth" ] && \
            set_timed_lock "$3" "$4"

        if check_nm && wordinlist "$1" "wifi wwan" && [ "$_rfkdev" = "1" ]; then
            # switch device with NetworkManager
            case "$newst" in
                1) invoke_nmcli "$1" on  ;;
                0) invoke_nmcli "$1" off ;;
            esac
            # record device state after nmcli
            get_devs "$1"

        elif [ "$_rfkdev" = "1" ] && cmd_exists $RFKILL ; then
            # switch device with rfkill
            if test_rfkill_perms ; then
                # use rfkill
                echo_debug "rf" "device_switch($1, $2).rfkill"
                case "$newst" in
                    1) $RFKILL unblock "$1" > /dev/null 2>&1 ;;
                    0) $RFKILL block "$1"   > /dev/null 2>&1 ;;
                    *) ;;
                esac
                # record device state after rfkill
                get_devs "$1"
            else
                # missing permission to rfkill
                err_no_root_priv "device_switch($1, $2).rfkill"
            fi
        else
            # switch device with direct write
            case "$newst" in
                1) devn="$_devon"  ;;
                0) devn="$_devoff" ;;
            esac
            if test_root ; then
                write_sysf "$devn" "$_devc"
                echo_debug "rf" "device_switch($1, $2).devc: rc=$?"
                # record device state after direct write
                get_devs "$1"
            else
                err_no_root_priv "device_switch($1, $2).devc"
            fi
        fi
    fi # states did not match

    # quit if device state is hard blocked or invalid
    if [ "$_devs" -ge 2 ]; then
        case "$_devs" in
            2) echo_debug "rf" "device_switch($1, $2).hard_blocked: rc=$_devs" ;;
            *) echo_debug "rf" "device_switch($1, $2).invalid_state: rc=$_devs" ;;
        esac
        return "$_devs"
    fi

    # compare old and new device state
    if [ "$curst" = "$_devs" ]; then
        # state did not change
        echo_debug "rf" "device_switch($1, $2).no_change: rc=4"
        return 4
    else
        echo_debug "rf" "device_switch($1, $2).ok: rc=0"
        return 0
    fi
}

echo_device_state () { # print radio type state -- $1: rftype, $2: state
    case "$1" in
        bluetooth)
            devstr="bluetooth"
            ;;

        nfc)
            devstr="nfc      "
            ;;

        wifi)
            devstr="wifi     "
            ;;

        wwan)
            devstr="wwan     "
            ;;

        *)
            devstr=$1
            ;;
    esac

    case "$2" in
        0)
            echo "$devstr = off (software)"
            ;;

        1)
            echo "$devstr = on"
            ;;

        2)
            echo "$devstr = off (hardware)"
            ;;

        254)
            echo "$devstr = none (no device)"
            ;;

        *)
            echo "$devstr = invalid state"
    esac

    return 0
}

# shellcheck disable=SC2120
save_device_states () { # save radio states -- $1: list of rftypes
                        # rc: 0=ok/1=create failed/2=write failed
    local dev
    local devlist="${1:-$ALLDEV}" # when arg empty -> use all
    local rc=0

    # create empty state file
    if [ -d "$VARDIR" ] && { : > "$RFSTATEFILE"; } 2> /dev/null; then
        # iterate over all possible devices -> save state in file
        for dev in $devlist; do
            device_state "$dev"
            { printf '%s\n' "$dev $_devs" >> "$RFSTATEFILE"; } 2> /dev/null || rc=2
        done
    else
        # create failed
        rc=1
    fi

    echo_debug "rf" "save_device_states($devlist): $RFSTATEFILE; rc=$rc"
    return $rc
}

restore_device_states () { # restore radio type states
                           # rc: 0=ok/1=state file nonexistent
    local sline
    local rc=0

    if [ -f "$RFSTATEFILE" ]; then
        # read state file
        # shellcheck disable=SC2162
        while read -r sline; do
            # shellcheck disable=SC2086
            set -- $sline # read dev, state into $1, $2
            device_switch "$1" "$2"
        done < "$RFSTATEFILE"
    else
        # state file nonexistent
        rc=1
    fi

    echo_debug "rf" "restore_device_states: $RFSTATEFILE; rc=$rc"
    return $rc
}

set_radio_device_states () { # set/initialize all radio states
    # $1: start/stop/1/0/radiosw
    # called from init scripts or upon change of power source
    local dev devs2disable devs2enable restore
    local quiet=0

    # save/restore mode is disabled by default
    if [ "$1" != "radiosw" ]; then
        restore="$RESTORE_DEVICE_STATE_ON_STARTUP"
    else
        restore=0
    fi

    if [ "$restore" = "1" ]; then
        # "save/restore" mode
        echo_debug "rf" "set_radio_device_states($1).restore"
        case $1 in
            start)
                if restore_device_states; then
                    echo "Radio device states restored."
                else
                    echo "No saved radio device states found."
                fi
                ;;

            stop)
                # shellcheck disable=SC2119
                save_device_states
                echo "Radio device states saved."
                ;;
        esac
    else
        # "disable/enable on startup/shutdown or bat/ac" or "radiosw" mode
        case $1 in
            start) # system startup
                devs2disable="$DEVICES_TO_DISABLE_ON_STARTUP"
                devs2enable="$DEVICES_TO_ENABLE_ON_STARTUP"
                ;;

            stop) # system shutdown
                devs2disable=""
                devs2enable=""

                if [ "$X_WIFI_ON_SHUTDOWN" != "0" ]; then
                    # NM workaround: if
                    # 1. disable wifi is configured somehow, and
                    # 2. wifi is not explicitly configured for shutdown
                    # then re-enable wifi on shutdown to prepare for startup
                    if wordinlist "wifi" "$DEVICES_TO_DISABLE_ON_BAT
                                          $DEVICES_TO_DISABLE_ON_BAT_NOT_IN_USE
                                          $DEVICES_TO_DISABLE_ON_LAN_CONNECT
                                          $DEVICES_TO_DISABLE_ON_WIFI_CONNECT
                                          $DEVICES_TO_DISABLE_ON_WWAN_CONNECT" && \
                       ! wordinlist "wifi" "$devs2disable $devs2enable"; then
                        devs2enable="wifi $devs2enable"
                    fi
                fi
                ;;

            1) # battery power --> build disable list
                quiet=1 # do not display progress
                devs2enable=""
                devs2disable="${DEVICES_TO_DISABLE_ON_BAT:-}"

                # check configured list for connected devices
                for dev in ${DEVICES_TO_DISABLE_ON_BAT_NOT_IN_USE:-}; do
                     # if device is not connected and not in list yet --> add to disable list
                    { case $dev in
                        bluetooth) any_bluetooth_in_use ;;
                        wifi) any_wifi_in_use ;;
                        wwan) any_wwan_in_use ;;
                    esac } || wordinlist "$dev" "$devs2disable" || devs2disable="$dev $devs2disable"
                done
                devs2disable="${devs2disable# }"
                ;;

            0) # AC power --> build enable list
                quiet=1 # do not display progress
                devs2enable="${DEVICES_TO_ENABLE_ON_AC:-}"
                devs2disable=""
                ;;

            radiosw)
                devs2disable=""
                devs2enable="$DEVICES_TO_ENABLE_ON_RADIOSW"
                ;;
        esac

        echo_debug "rf" "set_radio_device_states($1): enable=$devs2enable disable=$devs2disable"

        # disable configured radios
        if [ -n "$devs2disable" ]; then
            [ "$quiet" = "1" ] || echo -n "Disabling radios:"
            for dev in bluetooth nfc wifi wwan; do
                if wordinlist "$dev" "$devs2disable"; then
                    [ "$quiet" = "1" ] || printf ' %s' "$dev"
                    device_switch $dev off
                fi
            done
            [ "$quiet" = "1" ] || echo "."
        fi

        # enable configured radios
        if [ -n "$devs2enable" ]; then
            if [ "$1" = "radiosw" ]; then
                # radiosw mode: disable radios not listed
                for dev in bluetooth nfc wifi wwan; do
                    if ! wordinlist "$dev" "$devs2enable"; then
                        device_switch $dev off
                    fi
                done
            else
                # start mode: enable listed radios
                [ "$quiet" = "1" ] || echo -n "Enabling radios:"
                for dev in bluetooth nfc wifi wwan; do
                    if wordinlist "$dev" "$devs2enable"; then
                        [ "$quiet" = "1" ] || printf ' %s' "$dev"
                        device_switch $dev on
                    fi
                done
                [ "$quiet" = "1" ] || echo "."
            fi
        fi

        # clean up: discard state file
        rm -f "$RFSTATEFILE" 2> /dev/null
    fi

    return 0
}
